"""
中标（Winning）文件处理脚本

功能：
- 将爬取的子详情页数据 SubDetailItem 输入给通义千问（Qwen）模型
- 支持自定义提示词（instruction / system）与输出格式（output_format）
- 返回结构化文本（JSON/YAML/自定义说明），可选择流式或非流式输出
"""

# 统一配置默认提示词，便于在文件顶部快速修改
# 系统提示：定义模型角色为“中标记录识别与抽取”的专员
DEFAULT_SYSTEM_MSG = (
    "你是一名专门识别与抽取中标/成交记录的结构化信息工程师。"
    "请严谨、客观、遵循指定输出格式；只依据提供的页面内容与上下文，不要编造。"
    "对于省份、城市等信息，如果给定信息太少，可以根据项目名称进行推断。"
)

# 任务指令：要求从页面内容中抽取中标信息，并保证字段准确与可用
DEFAULT_INSTRUCTION = (
    "从给定的页面HTML与上下文中识别中标/成交记录，按输出格式抽取对应字段。"
    "对金额、日期、币种等进行规范化；无法确定的字段置为 null 或空字符串，避免猜测。"
)

# 输出格式：与 WinningResultAnnouncement 模型字段对齐，并支持联合体公司
DEFAULT_OUTPUT_FORMAT = (
    "输出 JSON，字段与含义如下（与 WinningResultAnnouncement 模型对齐）：\n"
    "announcement_date：公告日期，优先使用 YYYY-MM-DD（如含时间可用 ISO8601），无法确定则 null；\n"
    "original_url：原始URL；\n"
    "winning_company_name：主中标公司名称（字符串），你需要过滤掉可能不是公司名称的信息，无法确定则 null；\n"
    "winning_companies：联合体公司列表（数组），元素可为字符串公司名或对象 {company_name: string}；无法确定则空数组；\n"
    "winning_amount：中标金额（数值，单位为元，Decimal 语义），若原文为万元/千元需换算，无法确定则 null；\n"
    "project_code：项目编号（字符串，可为空）；\n"
    "project_name：项目名称（字符串，可为空）；\n"
    "winning_info：对象（JSON），包含辅助信息：buyer（采购人/招标人）、suppliers（数组，列出所有中标供应商）、"
    "currency（币种，如 CNY/USD）、summary（一句话摘要）、attachments（数组，相关附件/链接，可为空）、notes（字符串，备注，可为空）。\n"
    "规范：金额统一换算为元；日期无法确定时返回 null；字符串不确定时返回空字符串或 null；suppliers/联合体均支持多家。"
)

from typing import Dict, Iterable, List, Optional
from spider.models import SubDetailItem
from process.ai.unified import chat_with_usage
from deal.models import WinningResultAnnouncement, Company, Project
from django.utils import timezone
from decimal import Decimal
import json
import re
from datetime import datetime
from uuid import uuid4


def _item_dict(item: SubDetailItem) -> Dict[str, str | int | bool]:
    """将 ORM 对象转为轻量字典，便于拼装提示词内容。"""
    return {
        "url": item.url,
        "html_content": item.html_content,
        "detail_item_id": item.detail_item_id,
        "task_id": item.task_id,
        "created_at": item.created_at.isoformat() if item.created_at else "",
        "return_count": item.return_count,
        "is_processed": item.is_processed,
    }


def build_messages(
    item: SubDetailItem,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    system: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    json_payload: Optional[Dict] = None,
) -> List[Dict[str, str]]:
    """构建发送给模型的消息列表。

    预留三个可自定义的提示位：
    - system：系统提示，定义模型角色与总体行为
    - instruction：具体任务指令（例如抽取中标信息）
    - output_format：输出格式约定（例如 JSON/YAML 字段约束）
    """
    sys_msg = system or DEFAULT_SYSTEM_MSG
    inst = instruction or DEFAULT_INSTRUCTION
    fmt = output_format or DEFAULT_OUTPUT_FORMAT
    data = _item_dict(item)
    ctx = extra_context or {}
    payload = json_payload if json_payload is not None else data
    user_content = (
        "Instruction:\n" + inst + "\n" +
        "OutputFormat:\n" + fmt + "\n" +
        "Context:\n" + str(ctx) + "\n" +
        "URL:\n" + data["url"] + "\n" +
        "JSON:\n" + json.dumps(payload, ensure_ascii=False)
    )
    return [
        {"role": "system", "content": sys_msg},
        {"role": "user", "content": user_content},
    ]


def process_winning(
    item: SubDetailItem,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    system: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    model: str = "qwen-plus",
    stream: bool = False,
    **kwargs,
) -> str | Iterable[str]:
    """处理单个 SubDetailItem，调用通义千问模型生成结构化文本。

    - 非流式：返回完整字符串
    - 流式：返回迭代器，逐片输出片段
    其余关键字参数（kwargs）将透传给底层 chat 调用，例如 temperature、top_p 等。
    """
    messages = build_messages(
        item=item,
        instruction=instruction,
        output_format=output_format,
        system=system,
        extra_context=extra_context,
        json_payload=json_payload,
    )
    return chat_with_usage(messages=messages, model=model, **kwargs).get("content")


def _parse_json_payload(text: str) -> Dict:
    if not text:
        return {}
    s = text.strip()
    if s.startswith("```"):
        s = s.split("\n", 1)[1] if "\n" in s else s
        if s.endswith("```"):
            s = s[:-3].strip()
    s = re.sub(r"^\s*<think>[\s\S]*?</think>\s*", "", s)
    m = re.search(r"\{[\s\S]*\}", s)
    if m:
        frag = m.group(0)
        try:
            return json.loads(frag)
        except Exception:
            pass
    try:
        return json.loads(s)
    except Exception:
        return {}


def _parse_datetime(value: str | None) -> Optional[datetime]:
    """将字符串解析为 datetime（尽量容错）。"""
    if not value:
        return None
    value = value.strip()
    for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d", "%Y/%m/%d %H:%M:%S"):
        try:
            dt = datetime.strptime(value, fmt)
            return timezone.make_aware(dt) if timezone.is_naive(dt) else dt
        except Exception:
            continue
    try:
        dt = datetime.fromisoformat(value)
        return timezone.make_aware(dt) if timezone.is_naive(dt) else dt
    except Exception:
        return None


def _parse_amount(value) -> Optional[Decimal]:
    """解析金额为 Decimal（单位元）。支持字符串或数值。"""
    if value is None:
        return None
    try:
        if isinstance(value, (int, float, Decimal)):
            return Decimal(str(value))
        s = str(value).strip()
        # 去除逗号分隔
        s_clean = s.replace(",", "")
        return Decimal(s_clean)
    except Exception:
        return None


def process_and_ingest_winning(
    item: SubDetailItem,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    system: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    model: str = "qwen-plus",
    json_payload: Optional[Dict] = None,
    **kwargs,
) -> Dict[str, object]:
    """调用模型→校验数据→入库 WinningResultAnnouncement。

    返回结果包含：
    - ok: 是否成功
    - errors: 校验或入库错误列表
    - id: 成功时记录ID
    - action: "created" 或 "updated"
    """
    # 1) 调用模型（非流式）
    r = chat_with_usage(
        messages=build_messages(
            item=item,
            instruction=instruction,
            output_format=output_format,
            system=system,
            extra_context=extra_context,
            json_payload=json_payload,
        ),
        model=model,
        **kwargs,
    )
    text = r.get("content") or ""

    # 2) 解析与校验
    payload = _parse_json_payload(text)
    errors: List[str] = []
    if not payload:
        return {"ok": False, "errors": ["结构化输出不是合法 JSON"]}

    # 必填字段校验与解析
    ann_date = _parse_datetime(payload.get("announcement_date")) or item.created_at

    original_url = payload.get("original_url") or item.url

    # 支持联合体：优先解析列表字段，其次回退单一字段
    companies_input = payload.get("winning_companies") or payload.get("winning_companies_names")
    companies: List[Company] = []
    if isinstance(companies_input, list) and companies_input:
        for item_company in companies_input:
            if isinstance(item_company, dict):
                name = item_company.get("company_name") or f"未知公司-{uuid4().hex[:8]}"
            else:
                name = str(item_company).strip() or f"未知公司-{uuid4().hex[:8]}"
            c = Company.objects.filter(company_name=name).first()
            if not c:
                c = Company.objects.create(
                    company_name=name,
                    unified_social_credit_code=f"AUTO-{uuid4().hex[:12]}",
                    company_info={},
                    qualification_info={},
                )
            companies.append(c)
    else:
        company_name = payload.get("winning_company_name") or f"未知公司-{uuid4().hex[:8]}"
        c = Company.objects.filter(company_name=company_name).first()
        if not c:
            c = Company.objects.create(
                company_name=company_name,
                unified_social_credit_code=f"AUTO-{uuid4().hex[:12]}",
                company_info={},
                qualification_info={},
            )
        companies.append(c)

    amount = _parse_amount(payload.get("winning_amount"))
    if amount is None:
        amount = Decimal("0")

    project_code = payload.get("project_code") or f"AUTO-{item.task_id}-{item.id}"
    project = Project.objects.filter(project_code=project_code).first()
    if not project:
        project = Project.objects.create(
            project_code=project_code,
            project_name=payload.get("project_name") or "未命名项目",
            owner_name=(payload.get("winning_info", {}) or {}).get("buyer") or "未知业主",
            proxy_company="",
            project_type="",
            service_period="",
            budget_amount=None,
            extra_data={},
            crawl_task=item.task,
        )

    # 仍可记录的附加字段
    project_name = payload.get("project_name") or ""
    info = payload.get("winning_info") or {}
    if not isinstance(info, dict):
        info = {"raw_winning_info": info}
    # 合并扩展信息
    info.update({
        "buyer": payload.get("buyer"),
        "suppliers": payload.get("suppliers"),
        "currency": payload.get("currency"),
        "summary": payload.get("summary"),
        "attachments": payload.get("attachments"),
        "notes": payload.get("notes"),
        "project_name": project_name,
    })

    if errors:
        return {"ok": False, "errors": errors}

    # 3) 入库（存在则更新，不存在则创建）。唯一键：announcement_date + project
    existing = WinningResultAnnouncement.objects.filter(
        announcement_date=ann_date, project=project
    ).first()
    if existing:
        existing.original_url = original_url
        existing.winning_company = companies[0]
        existing.winning_amount = amount
        existing.winning_info = info
        existing.crawl_task_id = item.task_id
        existing.save()
        try:
            existing.winning_companies.set(companies)
        except Exception:
            pass
        # 标记任务状态为已处理
        if item.task_id and item.task:
            item.task.status = "crawled"
            item.task.crawled_at = timezone.now()
            item.task.save(update_fields=["status", "crawled_at"]) 
        # 质量标记：缺少业主或金额
        info_buyer = info.get("buyer")
        if (not info_buyer) or (amount is None or amount <= 0):
            item.is_suspect = True
            item.save(update_fields=["is_suspect"]) 
        return {
            "ok": True,
            "id": existing.id,
            "action": "updated",
            "winning_companies": [c.company_name for c in companies],
            "summary": {
                "url": original_url,
                "project_name": project.project_name,
                "units": [existing.winning_company.company_name, info.get("buyer")],
                "amount": str(amount) if amount is not None else None,
            },
            "usage": r.get("usage") or {},
        }

    created = WinningResultAnnouncement.objects.create(
        announcement_date=ann_date,
        original_url=original_url,
        winning_company=companies[0],
        winning_amount=amount,
        project=project,
        winning_info=info,
        crawl_task_id=item.task_id,
    )
    try:
        created.winning_companies.set(companies)
    except Exception:
        pass
    if item.task_id and item.task:
        item.task.status = "crawled"
        item.task.crawled_at = timezone.now()
        item.task.save(update_fields=["status", "crawled_at"]) 
    info_buyer = info.get("buyer")
    if (not info_buyer) or (amount is None or amount <= 0):
        item.is_suspect = True
        item.save(update_fields=["is_suspect"]) 
    return {
        "ok": True,
        "id": created.id,
        "action": "created",
        "winning_companies": [c.company_name for c in companies],
        "summary": {
            "url": original_url,
            "project_name": project.project_name,
            "units": [created.winning_company.company_name, info.get("buyer")],
            "amount": str(amount) if amount is not None else None,
        },
        "usage": r.get("usage") or {},
    }


__all__ = ["build_messages", "process_winning", "process_and_ingest_winning"]


def process_winning_items(
    task_id: Optional[int] = None,
    only_unprocessed: bool = True,
    limit: Optional[int] = None,
    system: Optional[str] = None,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    model: str = "qwen-plus",
    **kwargs,
) -> Dict[str, object]:
    """批量处理 SubDetailItem 并入库，返回统计与明细。"""
    qs = SubDetailItem.objects.all()
    if task_id is not None:
        qs = qs.filter(task_id=task_id)
    if only_unprocessed:
        qs = qs.filter(is_processed=False)
    if limit is not None and limit > 0:
        qs = qs[:limit]

    results: List[Dict[str, object]] = []
    success = 0
    failed = 0

    for item in qs:
        try:
            result = process_and_ingest_winning(
                item=item,
                instruction=instruction,
                output_format=output_format,
                system=system,
                extra_context=extra_context,
                model=model,
                **kwargs,
            )
        except Exception as e:
            result = {"ok": False, "errors": [f"处理异常: {e!r}"]}

        item.return_count = (item.return_count or 0) + 1
        if result.get("ok"):
            item.is_processed = True
            success += 1
        else:
            item.is_processed = False
            failed += 1
        item.save()

        results.append({
            "item_id": item.id,
            "ok": result.get("ok"),
            "action": result.get("action"),
            "errors": result.get("errors"),
            "record_id": result.get("id"),
        })

    return {
        "total": len(results),
        "success": success,
        "failed": failed,
        "results": results,
    }


def run_winning_process(
    task_id: Optional[int] = None,
    limit: Optional[int] = None,
    model: str = "qwen-plus",
    system: Optional[str] = None,
    instruction: Optional[str] = None,
    output_format: Optional[str] = None,
    extra_context: Optional[Dict[str, str]] = None,
    **kwargs,
) -> Dict[str, object]:
    """一句话入口：筛选→调用→校验→入库→返回统计。"""
    return process_winning_items(
        task_id=task_id,
        only_unprocessed=True,
        limit=limit,
        model=model,
        system=system,
        instruction=instruction,
        output_format=output_format,
        extra_context=extra_context,
        **kwargs,
    )


__all__ = [
    "build_messages",
    "process_winning",
    "process_and_ingest_winning",
    "process_winning_items",
    "run_winning_process",
]
